Design ASP.NET Pages and Controls That Take Advantage of the DHTML Object Model

 

Dino Esposito
Wintellect

November 2003

Summary: Learn how to build an enhanced Panel control with the capability of hiding and displaying its contents. The control makes intensive use of DHTML client script code to implement the feature. The control emits the script code during the rendering phase. It also uses a hidden field to persist the client-side state (whether or not the view is collapsed) and the methods of the IPostBackDataHandler interface to interact with the ASP.NET server environment. (11 printed pages)

Contents

Introduction An Expandable Panel Control
Rendering the Output
Persisting the Client-side State
Summary

Download the source code for this article (112 KB).

Introduction

Part I of this article is primarily devoted to researching effective ways to integrate client-side functionality in Microsoft® ASP.NET pages. The ASP.NET page object model provides several ready-to-use methods to insert custom JavaScript code in the response text. Smart developers wanting to build smart controls typically exploit that extensively. If you have ever played with the Internet Explorer Web Controls (including TreeView, Toolbar, TabStrip, and MultiPage), you know what I mean. (If you are not familiar with these controls, download them at https://www.asp.net/IEWebControls/Download.aspx and run some of the included examples.) Internet Explorer Web controls are heavily based on DHTML functionality, leverage the rich DHTML object model, and let you create advanced, desktop-like functions normally unavailable over the Web. For example, the TreeView Web control looks exactly like its Win32 counterpart and lets you expand and collapse nodes without roundtripping to the server to download further information.

Client-side functionality requires deep support from the browser, and detecting browser capabilities is the first task that DHTML-dependent pages and controls should accomplish. In the first part of this article, Part I: Browser-Sensitive Pages, we discussed techniques to make a page implement a given function in different ways according to the characteristics of the underlying browser. Here in Part 2, I'm going to write a special ASP.NET control that makes intensive use of DHTML functionality. I'll take an existing control—Panel—and enhance it with client-side code. In particular, the new panel control will have the capability of hiding and displaying its contents, in much the same way in which Windows® XP folder panels do.

Aa479327.dhtmlobjectmodel201(en-us,MSDN.10).gif

Figure 1. The Windows XP folder panel that the Panel control will mimic

When it comes to DHTML integration, there are two main tasks you should consider for implementation—generation of the script code and persistence of the client-side changes. For example, the Panel control can be initially displayed to the users in a collapsed form. Next, the user expands its contents (that is, the client-side state is modified), clicks somewhere in the page, and the page posts back. At this point, wouldn't it be nice if the new page could reflect the last state (expanded or collapsed) of the panel? If this feature sounds too complex or perhaps sophisticated, consider that this functionality is equivalent to what drop-down list and textbox controls do when the page that contains them posts back.

Writing DHTML-enabled controls consists of two main steps—adding any JavaScript code needed to implement the desired feature, and exploiting any ASP.NET-specific mechanism to inform the server-side code of the control of the changes occurring on the client. Let's see how these two steps can be implemented in practice.

An Expandable Panel Control

Based on the built-in System.Web.UI.WebControls.Panel class, the new control wraps the standard markup in an outermost <table> element. The standard markup generated by the base Panel class is embedded in the parent element as a row. Another topmost row contains a clickable element to let users hide and display the contents of the panel. The following HTML code shows the final layout.

<table>
   <tr>
      <td>title</td>
      <td>button</td>
   </tr>
   <tr>
      <td colspan=2> markup of the base Panel control </td>
   </tr>
</table>

The outermost table is always visible; the user determines whether the contents are visible or not. By default, the markup of the base Panel control is wrapped in a <div> element. To make the content appear and disappear as the user clicks a button, you need to adjust the display attribute of its style property. The following code snippet shows the script code that make the contents of the panel appear and disappear.

// Make the panel disappear
theDiv.style["display"] = "none";
// Make the panel appear
theDiv.style["display"] = "";

The style property must be set on the DHTML object that represents the HTML <div> tag. In the code snippet above, this object is named theDiv, which is the value of the ID property of the Panel server control.

Table 1 shows the list of custom properties defined on the new control.

Property Description
CaptionDithered Indicates whether the background of the panel's title bar has to be dithered. (A horizontal gradient of colors is used.)
ClickButtonColor Gets and sets the color for the text of the expand/collapse button.
Closed Determines the initial display state of the panel—closed or expanded. False by default.
Expandable Indicates whether the panel will feature the expand/collapse feature. If this property is false, the control works like the classic panel. True by default.
Margin Gets and sets the distance in pixels of the contents from the borders of the panel.
RightToLeft Indicates the direction of the gradient if CaptionDithered is set to true. False by default.
Title Gets and sets the title of the panel.

The key property is Expandable. If that property is set to false, then the control will behave just as its parent class would do. The custom Panel control overrides the OnLoad method on the base class to perform a check on the browser's capabilities. If the browser is not Internet Explorer 5.0 or later, the Expandable property is officially set to false.

protected override void OnLoad(EventArgs e) 
{
   // Call the base method
   base.OnLoad(e);

   // Check the browser caps and disable collapse/expand if needed
   bool uplevel = false;
   HttpBrowserCapabilities caps = Page.Request.Browser;
   if (caps.Browser.ToUpper().IndexOf("IE") > -1) {
      // This is IE. But is it at least version 5?
      if (caps.MajorVersion >4)
         uplevel = true;
   }

   // If the browser is not IE5 or higher, drop collapse/expand 
   if (!uplevel) 
      Expandable = false;
}

Rendering the Output

Once you are sure about the browser support, you can build the new control tree. The method responsible for the control's output is Render. The overall structure of the method is shown below.

protected override void Render(HtmlTextWriter output)      
{
   if (!Expandable) {
base.Render(output);
return;
   }

   // Build the new control tree
   :
}

The control tree is a hierarchy of ASP.NET objects that mimic the HTML layout described earlier. You capture to a string the default output of the base Panel control (and the output of any other ASP.NET control) using the following code:

StringWriter writer = new StringWriter();
HtmlTextWriter buffer = new HtmlTextWriter(writer);
base.Render(buffer);
string panelOutput = writer.ToString();

This markup code is merged with a new, dynamically created Table control. The outermost table takes in many of the visual styles of the Panel control, including font, width, and color settings. The table also defines a custom 3D border, one pixel wide. The color of the border is the value of the Panel's BorderColor property. For a nice-looking effect on the title bar of the panel, the user can set the CaptionDithered property to true, which results in a DHTML filter style being set. The following method is in charge of this critical operation.

void BuildControlTree(HtmlTextWriter output, 
string id, string panelOutput)
{
   Table t = new Table();
   t.CellPadding = 0;
   t.CellSpacing = 0;
   t.BorderColor = BorderColor;
   t.BorderStyle = BorderStyle.Outset;
   t.BorderWidth = Unit.Pixel(1);
   t.ForeColor = ForeColor;
   t.Font.Name = Font.Name;
   t.Font.Size = Font.Size;
   t.Width = Width;
   if (CaptionDithered)
   {
      string filter = "progid:DXImageTransform.Microsoft.Gradient" + 
                            "(gradienttype=1, startColorstr='{0}', " +       
                            "endColorstr='{1}'"; 
      if (RightToLeft)
         t.Style["filter"] = String.Format(filterString, 
            Color.Snow.Name, 
            BorderColor.Name); 
      else
         t.Style["filter"] = String.Format(filterString, 
            BorderColor.Name, 
            Color.Snow.Name);
   }

   // Prepare the topmost row
   TableRow rowTop = new TableRow();
   if (!CaptionDithered)
      rowTop.BackColor = t.BorderColor;

   // Leftmost cell: title
   TableCell cellTop = new TableCell();
   cellTop.Text = String.Format("<b>&nbsp;{0}</b>", Title);
   rowTop.Cells.Add(cellTop);

   // Rightmost cell: expand/collapse button
   TableCell cellTopRight = new TableCell();
   cellTopRight.HorizontalAlign = HorizontalAlign.Right;
   LiteralControl imgRight = CreateClientButton(); 
   if (imgRight != null)
      cellTopRight.Controls.Add(imgRight);
   rowTop.Cells.Add(cellTopRight);

   // Add the top row to the table
   t.Rows.Add(rowTop);

   // Insert the Panel's markup in the table cell 
   TableRow rowBody = new TableRow();
   TableCell cellBody = new TableCell();
   cellBody.BackColor = Color.White;
   cellBody.ColumnSpan = 2;
   cellBody.Text = panelOutput;

   rowBody.Cells.Add(cellBody);
   t.Rows.Add(rowBody);

   // Output
   t.RenderControl(output);
}

The BuildControlTree method adds two rows to the outermost table. The topmost row is the panel title bar and counts two cells. The leftmost cell contains only the title of the panel rendered using the ForeColor base property. The rightmost cell contains a clickable button bound to a bit of JavaScript code. In the code above, the clickable button is rendered as a LiteralControl that brings in the following markup code:

<a href='javascript:Toggle_ctl0()'>
    <span style='color:DodgerBlue;font-family:webdings;'>2</span>
</a> 

When the button is clicked, the JavaScript procedure runs and the visibility of the next <div> block is toggled on and off. Note that the name of the JavaScript function—Toggle_ctl0 in the sample—reflects the ID of the panel it refers to. This is a key point for persisting client-side changes to the server. The "2" string in the Webdings character set renders as 2. Want to have a look at the control as it is coming along? Here's a sneak preview in Figure 2.

Aa479327.dhtmlobjectmodel202(en-us,MSDN.10).gif

Figure 2. A DHTML-enabled version of the ASP.NET Panel control. The control features a button to expand/collapse its contents.

The Panel contains a DataGrid that can be hidden from view by clicking on the button in the title bar. The markup required for this version of the panel control is shown below.

<msdn:Panel runat="server" 
Expandable="true" 
   Width="500px"
   Margin="10"
   Title="Employees">
<!-- any ASP.NET contents here -->
</msdn:panel>

The JavaScript code to toggle on and off the underlying block of markup is inserted using the RegisterClientScriptBlock method of the Page class. After creating the literal control that represents the clickable element, the above CreateClientButton function creates a string that represents a JavaScript function and adds it to the page. For a panel named _ctl0, and embedded in a server-side form named Form1, the JavaScript code looks like this:

<script language=JavaScript>
function Toggle_ctl0() {
var closed__ctl0 = document.forms["Form1"]["_ctl0"].value;
if (closed__ctl0.toLowerCase() == "true") {
_ctl0_theDiv.style["display"] = "";
document.forms["Form1"]["_ctl0"].value = false;
} else {
   _ctl0_theDiv.style["display"] = "none"; 
document.forms["Form1"]["_ctl0"].value = true;
}
}
</script>

So far, so good; the control works just fine and the contents is correctly toggled on and off as the user clicks. However, as soon as the page is refreshed the state of the panel is lost. Here's what can happen. The panel displays expanded; you click to collapse the contents; then the page posts back. When the page returns, the panel is expanded again. You must code some trick to persist the client-side state of the panel.

Persisting the Client-side State

Let me make a brief digression about how the state of a control is restored on the server. You might want to read my previous article The ASP.NET Page Object Model for more information. Each control can store in the ViewState bag all the server-side parameters that need be restored the next time the page is processed. In particular, the Panel control needs to track the value of the Closed property. The value of this property influences the visibility of the panel contents. By interacting with the page in the browser, the user can perform actions that alter the value of the property—for example, the user clicks to collapse the view. When the page posts back you need a way to track this client-side change and properly update the server-side Closed property. Sounds complicated? Actually, it's not, as this is exactly the mechanism that, say, a TextBox control implements to persist the text of the input field. So what makes the TextBox control different from the Panel control? The methods of the IPostBackDataHandler interface.

These methods are invoked after the view state of the page has been restored and basically let you bind a server-side property (that is, Closed) with a client-side input element. By supporting the interface, the programmer claims the responsibility of the actual implementation of the binding. What is the client-side input element you can bind the Closed property to? The standard markup generated for the panel doesn't include any input field, since the Panel control is simply a wrapper for the <div> tag. However, since we're writing a custom control, nothing prevents us from sneakily adding a hidden field.

// The name of the field matches the ID of the Panel control
Page.RegisterHiddenField(originalID, Closed.ToString());

If you look back at the JavaScript code discussed earlier, you can now make sense of those apparently obscure calls to document.forms. The client-side state of the expandable Panel control is read from, and written to, the hidden field that has the same name as the control. That hidden field is bound to the server-side Closed property in the LoadPostData method of the IPostBackDataHandler interface.

bool LoadPostData(string postDataKey, NameValueCollection postCollection) 
{
   // Compare current and posted value of Closed
   bool currentValue = Closed;
   bool postedValue = Convert.ToBoolean(postCollection[postDataKey]);

   // What if the field is empty?
   if (!currentValue.Equals(postedValue)) {
      Closed = postedValue;
      return true;
   }
   return false;
}

For the trick above to work, it is necessary that the ID of the hidden field matches the ID of the Panel control in the ASP.NET page. This ID is carried by the postDataKey argument. However, this is a limitation that has an easy workaround. In practice, you can give any name to the hidden field that you can track back in the LoadPostData method. You simply retrieve the posted value from the posted collection using your name instead of the postDataKey parameter. Although you can use any name to identify the hidden field, bear in mind that the name has to be unique for each control of the same type embedded in the form. A good technique is shown below.

protected string HiddenFieldID
{
get {return "__" + ClientID + "_State";}
}

You define a protected string member and make it return a string that includes the client-side name of the control.

The LoadPostData method is invoked by the .NET Framework during the restoration of the page state, before the page receives the Load event. The .NET Framework loops over the parameters in the Request.Form collection. Each key in the collection is matched to the ID of a server-side control defined in the page. When a match is found, if the server-side control supports the IPostBackDataHandler interface, the LoadPostData method is called. The first argument is set to the key of the matching entry in the Request.Form name/value collection. The second argument is the collection itself. The postedValue variable in the code above returns the value stored in the input field whose name matches the ID of the panel. By construction, this is our hidden field where the JavaScript code persisted the client-side state of the expandable panel. If the posted value differs from the server-side value of the Closed property, the property is updated. Then, when ASP.NET generates its output to the browser the display of the panel contents is the same as it was the last time. The figure below simply demonstrates that the panel retains its state even after a postback.

Aa479327.dhtmlobjectmodel203(en-us,MSDN.10).gif

Figure 3. The Panel control is collapsed and the page posts back. When the page returns, the state of the control is not altered.

The IPostBackDataHandler interface features a second method named RaisePostDataChangedEvent. This method is expected to raise a server-side event to notify the ASP.NET application that the state of the control has changed. For example, you can implement the method and make it fire a custom ClosedChanged server-side event so that page developers can write a proper handler to manage the new situation.

public virtual void RaisePostDataChangedEvent() {
   if (ClosedChanged != null)
ClosedChanged(this, EventArgs.Empty);
}

Summary

A DHTML control must fulfill two main tasks. First, it has to emit any JavaScript code that is needed to implement the desired feature. The client script must also be properly bound to the HTML elements that form the control output. In addition, the control should emit one or more hidden fields to persist the client-side state of the control and interact with the server-side plumbing of ASP.NET. The client script is responsible for injecting state information into the hidden fields at some point before the page posts back. The IPostBackDataHandler interface is the ASP.NET built-in mechanism by means of which DHTML controls can seamlessly share their state with the page object model. At the beginning of this article you can download the source code of the expandable Panel control along with a sample page. Enjoy!

About the Author

Dino Esposito is a trainer and consultant based in Rome, Italy. Member of the Wintellect team, Dino specializes in ASP.NET and ADO.NET and spends most of his time teaching and consulting across Europe and the United States. Dino manages the ADO.NET courseware for Wintellect and writes the "Cutting Edge" column for MSDN Magazine.

© Microsoft Corporation. All rights reserved.